///////////////////////////////////////////////////////////////
// Object creation method:
///////////////////////////////////////////////////////////////
var makeConnieMap = function(mapDivId, regions, fields, cbarURL, settings) {
  var that,                     // Object to return.
    map,                        // The actual map object.
    wktConverter,               // OL WKT reader/writer instance.
    geoProj,                    // The projection (geo) that we deal with, for converting for different base layers.
    mapProj,                    // The current map projection (don't change this manually)
    uvfields,                   // The UV fields (containing bounds, etc) in use.
    selectionLayer,             // Layer containing user selections.
    resultsLayer,               // Layer displaying the results plot.
    boundsLayer,                // Static layer displaying the available data bounds.
    cbarControl;                // Control displaying the colour scheme bar.
  settings = settings || {};

  that = {};

  wktConverter = new OpenLayers.Format.WKT();

  // Native projections are assumed to be geographic, as are the
  // lonlats, etc that we pass around.  However, the map may be in a
  // different projection so we need to convert to and from all the
  // time, between map-projection and geo.
  geoProj = new OpenLayers.Projection('EPSG:4326');

  ///////////////////////////////////////////////////////////////
  // Public methods:
  ///////////////////////////////////////////////////////////////

  /**
   * Method: getUserRegions
   * Get list of regions selected on the map.
   *
   * Returns:
   * {Array}
   */
  var getUserRegions = function() {
    var regions = [];
    var selectedFeatures = selectionLayer.features;
    if (selectedFeatures){
      for (var i = 0; i < selectedFeatures.length; i++){
        var bounds = selectedFeatures[i].geometry.getBounds().clone();
        bounds.transform(mapProj, geoProj);
        regions.push(bounds);
      }
    }
    return regions;
  };
  that.getUserRegions = getUserRegions;

  var setUserRegions = function(regions) {
    if (typeof regions === 'string') {
      regions = wktConverter.read(regions);
    }
    selectionLayer.removeAllFeatures();
    var transformed = regions.clone();
    transformed.transform(geoProj, mapProj);
    selectionLayer.addFeatures(transformed);
    selectionLayer.refresh({force:true});
  };
  that.setUserRegions = setUserRegions;

  var userSelectionExtent = function() {
    var regions = getUserRegions(),
        geoms = [];
    for (var i = 0; i < regions.length; i++) {
      geoms.push(regions[i].toGeometry());
    }
    var envelope = new OpenLayers.Geometry.Collection(geoms);
    envelope.calculateBounds();
    var envbounds = envelope.getBounds();
    envbounds.transform(mapProj, geoProj);
    return envbounds;
  };
  that.userSelectionExtent = userSelectionExtent;

  var setUVFields = function(fields) {
    // Fields are assumed to be geographic
    uvfields = fields;
    var geometry = uvfields.bounds.geometry.clone();
    geometry.transform(geoProj, mapProj);
    if (boundsLayer) {
      boundsLayer.removeAllFeatures();
      boundsLayer.addFeatures(new OpenLayers.Feature.Vector(geometry));
    }
    var centroid = geometry.getCentroid();
    map.panTo(new OpenLayers.LonLat(centroid.x, centroid.y));
    map.zoomToExtent(geometry.bounds);
  };
  that.setUVFields = setUVFields;

  var toggleSelectionVisibility = function(visible) {
    if (selectionLayer && map.getLayer(selectionLayer.id)) {
      var isVisible = selectionLayer.getVisibility();
      if (visible === undefined && visible === null) {
        visible = !isVisible;
      }
      selectionLayer.setVisibility(visible);
    }
  };
  that.toggleSelectionVisibility = toggleSelectionVisibility;

  var clear = function() {
    if (resultsLayer && map.getLayer(resultsLayer.id)) {
      map.removeLayer(resultsLayer);
    }
    if (selectionLayer) {
      selectionLayer.destroyFeatures();
      toggleSelectionVisibility(true); // force visible again in case it was hidden
    }
    $.event.trigger('mapChanged');
    $.event.trigger('detailsChanged');
    return false;
  };
  that.clear = clear;

  var setColourSchemeByIndex = function(idx) {
    cbarControl.setColourBarByIndex(idx);
  };
  that.setColourSchemeByIndex = setColourSchemeByIndex;

  var newResultsLayer = function(url, width, height, x0, y0, x1, y1, runid) {
    runid = runid || 0;
    if (resultsLayer && map.getLayer(resultsLayer.id)) {
      map.removeLayer(resultsLayer);
    }
    var bounds = new OpenLayers.Bounds(x0, y0, x1, y1);
    bounds.transform(geoProj, mapProj);
    resultsLayer = new OpenLayers.Layer.Image(
      "Connie Results",
      url,
      bounds,
      new OpenLayers.Size(width, height),
      {isBaseLayer:false, alwaysInRange:true, alpha:true});
    map.addLayer(resultsLayer);
    resultsLayer.events.on({
        'visibilitychanged': function(e) {
          toggleSelectionVisibility(!e.object.getVisibility());
        },
        'loadend': function() {
          $.event.trigger('conniefinished');
        },
        'loaderror': function() {
          $.event.trigger('connieerror', [runid]);
        }
    });
  };
  that.newResultsLayer = newResultsLayer;

  var setResultsURL = function(url) {
    if (resultsLayer && map.getLayer(resultsLayer.id)) {
      // Note that the corresponding `conniefinished' event is already triggered by the loadend event on the results tile:
      $.event.trigger('conniestarted');
      resultsLayer.setUrl(url);
    }
  };
  that.setResultsURL = setResultsURL;

  ///////////////////////////////////////////////////////////////
  // Utility methods, used in the constructor (below):
  ///////////////////////////////////////////////////////////////

  var boundsForLonLat = function(lonLat) {
    // Input and output is assumed to be geographic.
    var lonInc = uvfields.cellWidth;
    var latInc = uvfields.cellHeight;

    var x = lonLat.lon;
    var y = lonLat.lat;

    // find bounds for point by rounding down and up to the increments:
    var x1 = Math.floor(x / lonInc) * lonInc;
    var x2 = Math.ceil(x / lonInc) * lonInc;
    var y1 = Math.floor(y / latInc) * latInc;
    var y2 = Math.ceil(y / latInc) * latInc;

    var bounds = new OpenLayers.Bounds(x1, y1, x2, y2);
    return bounds.toGeometry();
  };

  var isInUVBounds = function(lonLat) {
    // Input is assumed to be map-native:
    var pt = new OpenLayers.Geometry.Point(lonLat.lon, lonLat.lat);
    pt.transform(mapProj, geoProj);
    // might conceivably be a Vector feature, or it might be an array of them:
    var bounds = uvfields.bounds;
    if (bounds.CLASS_NAME === 'OpenLayers.Feature.Vector') {
      return bounds.geometry.containsPoint(pt);
    }
    else {                      // else assume it's an array:
      for (var i = 0, ii = bounds.length; i < ii; i++) {
        if (bounds[i].CLASS_NAME === 'OpenLayers.Feature.Vector'
            && bounds[i].geometry.containsPoint(pt)) {
          return true;
        }
      }
    }
    return false;
  };

  var toggleSelectionForLonLat = function(lonLat) {
    // Input is in map-native coordinates:
    // first get it working, then make it fast (if needed):
    if (isInUVBounds(lonLat)) {
      var currentFeatures = selectionLayer.features;
      var found = false;
      for (var i = 0; i < currentFeatures.length; i++) {
        if (currentFeatures[i].geometry.getBounds().containsLonLat(lonLat)) {
          // found something already at clicked point; remove that one:
          found = true;
          selectionLayer.removeFeatures(currentFeatures[i]);
          break;
        }
      }
      // nothing at that point; add one:
      if (!found) {
        lonLat.transform(mapProj, geoProj);
        var selectedCellBounds = boundsForLonLat(lonLat);
        selectedCellBounds.transform(geoProj, mapProj);
        var cellFeature = new OpenLayers.Feature.Vector(selectedCellBounds);
        selectionLayer.addFeatures(cellFeature);
      }
      // lastly, trigger events to notify of changes:
      $.event.trigger('detailsChanged');
      $.event.trigger('mapChanged');
    }
  };

  regions.bind('mapChanged', function() {
    var currentFeatures = selectionLayer.features,
      regionsTxt;
    if (currentFeatures.length === 0) {
      regionsTxt = '';
    }
    else {
      var multipoly = new OpenLayers.Geometry.MultiPolygon();
      for (var i = 0; i < currentFeatures.length; i++) {
        multipoly.addComponent(currentFeatures[i].geometry.clone());
      }
      multipoly.transform(mapProj, geoProj);
      var vectorFeature = new OpenLayers.Feature.Vector(multipoly);
      regionsTxt = wktConverter.write(vectorFeature);
    }
    regions.text(regionsTxt);
  });

  // The custom control that handles the discrete region selection by the user:
  var selectionControl = new OpenLayers.Control();
  // We need to do this in two stages (rather than pass the handler in as an
  // argument), because the handler needs to be given the control as an
  // initialisation parameter, and the control hasn't been initialised yet:
  OpenLayers.Util.extend(selectionControl, {
    handler: new OpenLayers.Handler.Click(
      selectionControl, {
        'click': function (evt) {
          var lonlat = map.getLonLatFromViewPortPx(evt.xy);
          // Pass the coordinate in as map-native:
          toggleSelectionForLonLat(lonlat);
       }},
      {})
  });

  // Create a custom colour-bar control:
  cbarControl = new OpenLayers.Control();
  OpenLayers.Util.extend(cbarControl, {
    draw: function() {
      OpenLayers.Control.prototype.draw.apply(this, arguments);
      if (this.imgelem == null) {
        this.imgelem = document.createElement("img");
        this.imgelem.src = cbarURL;
        this.div.appendChild(this.imgelem);
        this.div.className += " colourBar";
      }
      return this.div;
    },
    setColourBarByIndex: function(cbarIdx) {
      this.imgelem.src = cbarURL + '?cmap=' + cbarIdx;
    },
    toggleVisibility: function(force) {
      this.visible = this.visible || true;
      if (force !== undefined) {
        this.visible = force;
      } else {
        this.visible = !this.visible;
      }
      this.imgelem.style.visibility = this.visible ? "visible" : "hidden";
    },
    destroy: function() {
      if (this.imgelem != null) {
        this.div.removeChild(this.imgelem);
      }
      OpenLayers.Control.prototype.destroy.apply(this, arguments);
    }
  });

  var mapBaseLayerChanged = function() {
    if (!uvfields) return;

    // Important: at this point, mapProj is still in the *old*
    // projection, which we use to update things here:
    var oldMapProj = mapProj;
    mapProj = map.getProjectionObject();

    var geometry = uvfields.bounds.geometry.clone();
    geometry.transform(geoProj, mapProj);
    geometry.calculateBounds();
    var bounds = geometry.bounds;
    var zoom = map.getZoomForExtent(bounds, true);
    map.setCenter(bounds.getCenterLonLat(), zoom);

    // Change the results (image) layer too, if present:
    if (resultsLayer) {
      resultsLayer.extent.transform(oldMapProj, mapProj);
    }

    // now update the vector layers:
    var layers = [selectionLayer, boundsLayer];
    for (var lyr, i = 0, len = layers.length; i < len; i++) {
      lyr = layers[i];
      var features = lyr.features;
      for (var j = 0, flen = features.length, feature; j < flen; j++) {
        feature = features[j];
        feature.geometry.transform(oldMapProj, mapProj);
      }
      lyr.refresh({force:true});
    }
  };

  ///////////////////////////////////////////////////////////////
  // The "constructor"(not public, of course; just invoked the once):
  ///////////////////////////////////////////////////////////////
  var init = function() {
    map = new OpenLayers.Map(mapDivId, {
      displayProjection: 'EPSG:4326',
      eventListeners: {
        'changebaselayer': mapBaseLayerChanged
      }
    });

    var metacarta = new OpenLayers.Layer.WMS("Basic",
                                             "http://vmap0.tiles.osgeo.org/wms/vmap0",
                                             {layers: 'basic'},
                                             {wrapDateLine: true, isBaseLayer: true});
    var osm = new OpenLayers.Layer.OSM("OSM");

    var bmng = new BMNGLayer();
    // Definitely a hack; because the contrast between to two layers
    // is so great we add a class indicating whether the dark one is in
    // effect, so we can toggle any on-map styling we might want to:
    bmng.events.register('visibilitychanged',
                         null, // don't care about context
                         function (e) {
                           $('#' + mapDivId).toggleClass('bmng', e.object.getVisibility());
                         });

    boundsLayer = new OpenLayers.Layer.Vector('Selection Bounds',
                                              {'displayInLayerSwitcher':false,
                                              style: {
                                                'fillOpacity': 0,
                                                'strokeOpacity': 1,
                                                'strokeColor':'#ee9900',
                                                'strokeWidth':2}});

    selectionLayer = new OpenLayers.Layer.Vector("selections", {'displayInLayerSwitcher':false});

    map.addLayers([osm, metacarta, bmng, boundsLayer, selectionLayer]);
    mapProj = map.getProjectionObject();

    // Add in user (discrete) region selection:
    map.addControl(selectionControl);
    selectionControl.activate();

    // Basic map widgets:
    map.addControl(new OpenLayers.Control.PanZoomBar());
    map.addControl(new OpenLayers.Control.MousePosition());
    map.addControl(new OpenLayers.Control.ScaleLine());
    map.addControl(new OpenLayers.Control.LayerSwitcher());

    // Add the colour bar:
    map.addControl(cbarControl);

    // Hack: when the message box is closed, the map isn't updating
    // its size, which mainly manifests itself as selections being
    // vertically displaced.  Instead, catch the manually-triggered
    // global event and resize manually:
    $(window).resize(function(){
      map.updateSize();
    });

    var zoom = map.getZoomForExtent(new OpenLayers.Bounds(111, -45, 156, -9), true);
    map.setCenter(new OpenLayers.LonLat(134, -26), zoom);
  };
  init();

  return that;
};


/**
 * The blue-marble tile server from NASA is now no-longer a proper
 * WMS, although it uses the same syntax: it is now a WMTS, but not as
 * supported by OpenLayers (I haven't checked who is in the wrong
 * here).  See http://onearth.jpl.nasa.gov/tiled.html
 *
 * This class simply overrides the bit of the regular WMS layer to use the
 * correct URL and tile-origin.
 */
var BMNGLayer = OpenLayers.Class(OpenLayers.Layer.WMS, {
  initialize: function() {
    OpenLayers.Layer.WMS.prototype.initialize.apply(this, [
        "NASA Blue Marble (NG)",
        "", {}, {
          tileSize: new OpenLayers.Size(480, 480),
          resolutions: [0.533333333333, 0.266666666667, 0.133333333334, 0.066666666667, 0.0333333333335, 0.0166666666668, 8.3333333334e-3, 4.1666666667e-3],
          tileOriginCorner: 'tl',
          transitionEffect: 'resize' // not necessary, but nicer
    }]);
  },
  getImageSize: function() {
    return { w:480, h:480 };
  },
  getURL: function(bounds) {
    // Yes, they use this funny ordering (not quite as documented
    // either).  Note that this does result in numbers serialised like
    // '-51.9999999984', but that doesn't seem to hurt it.
    var nasaBounds = [bounds.left, bounds.bottom, bounds.right, bounds.top].join(',');
    return 'http://wms.jpl.nasa.gov/wms.cgi?request=GetMap&layers=BMNG&srs=EPSG:4326&format=image/jpeg&styles=Feb&width=480&height=480&bbox=' + nasaBounds;
  }
});
